/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.appium.android.bootstrap.handler; import android.os.SystemClock; import android.view.InputDevice; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import io.appium.android.bootstrap.AndroidCommand; import io.appium.android.bootstrap.AndroidCommandResult; import io.appium.android.bootstrap.AndroidElement; import io.appium.android.bootstrap.CommandHandler; import io.appium.android.bootstrap.Logger; import io.appium.android.bootstrap.WDStatus; import io.appium.uiautomator.core.UiAutomatorBridge; import java.lang.RuntimeException; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import static io.appium.android.bootstrap.utils.API.API_18; public class MultiPointerGesture extends CommandHandler { private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5; private PointerCoords createPointerCoords(final JSONObject obj) throws JSONException { final JSONObject o = obj.optJSONObject("touch"); if (o == null) { return null; } final int x = o.getInt("x"); final int y = o.getInt("y"); final PointerCoords p = new PointerCoords(); p.size = 1; p.pressure = 1; p.x = x; p.y = y; return p; } @Override public AndroidCommandResult execute(final AndroidCommand command) throws JSONException { try { final PointerCoords[][] pcs = parsePointerCoords(command); if (command.isElementCommand()) { final AndroidElement el = command.getElement(); if (el.performMultiPointerGesture(pcs)) { return getSuccessResult("OK"); } else { return getErrorResult("Unable to perform multi pointer gesture"); } } else { if (API_18) { if (performMultiPointerGesture(pcs)) { return getSuccessResult("OK"); } else { return getErrorResult("Unable to perform multi pointer gesture"); } } else { Logger.error("Device does not support API < 18!"); return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, "Cannot perform multi pointer gesture on device below API level 18"); } } } catch (final Exception e) { Logger.debug("Exception: " + e); e.printStackTrace(); return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage()); } } private PointerCoords[] gesturesToPointerCoords(final JSONArray gestures) throws JSONException { // gestures, e.g.: // [ // {"touch":{"y":529.5,"x":120},"time":0.2}, // {"touch":{"y":529.5,"x":130},"time":0.4}, // {"touch":{"y":454.5,"x":140},"time":0.6}, // {"touch":{"y":304.5,"x":150},"time":0.8} // ] // From the docs: // "Steps are injected about 5 milliseconds apart, so 100 steps may take // around 0.5 seconds to complete." ArrayList<PointerCoords> pc = new ArrayList(); PointerCoords lastPosition = null; int i = 1; JSONObject current = gestures.getJSONObject(0); double currentTime = current.getDouble("time"); double runningTime = 0.0; final int gesturesLength = gestures.length(); while (true) { if (runningTime > currentTime) { if (i == gesturesLength) { break; } current = gestures.getJSONObject(i++); currentTime = current.getDouble("time"); } PointerCoords currentCoord = createPointerCoords(current); // Check if current action has no position (waiting before starting gesture) if (currentCoord != null) lastPosition = currentCoord; pc.add(lastPosition); runningTime += 0.005; } return pc.toArray(new PointerCoords[0]); } private PointerCoords[][] parsePointerCoords(final AndroidCommand command) throws JSONException { final JSONArray actions = (org.json.JSONArray) command.params().get( "actions"); final PointerCoords[][] pcs = new PointerCoords[actions.length()][]; for (int i = 0; i < actions.length(); i++) { pcs[i] = gesturesToPointerCoords(actions.getJSONArray(i)); } return pcs; } // Based on https://android.googlesource.com/platform/frameworks/uiautomator/+/61ce05bd4fd5ffc1f036c7c02c9af7cb92d6ec50/src/com/android/uiautomator/core/InteractionController.java#686 // But supports actions with pointers starting and ending at different moments. private int getPointerAction(int motionEvent, int index) { // Creates a pointer action in multi pointer events. // Notice that the index argument is the index of the touch up/down event // inside the array and not the pointer id (even the docs were confusing, // since the constant had a misleading ACTION_POINTER_ID_SHIFT name in early // API versions). return motionEvent + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT); } private boolean injectEventSync(MotionEvent event) { return UiAutomatorBridge.getInstance().injectInputEvent(event, true); } private boolean injectPointers(long downTime, int action, final List<PointerProperties> properties, final List<PointerCoords> coords) { // Injects pointers using some default values. Number of pointers is assumed // to be the length of the coords list. final MotionEvent event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), action, coords.size(), properties.toArray(new PointerProperties[0]), coords.toArray(new PointerCoords[0]), 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEventSync(event); } private PointerProperties fingerProperty(int id) { PointerProperties prop = new PointerProperties(); prop.id = id; prop.toolType = MotionEvent.TOOL_TYPE_FINGER; return prop; } private int findIndex(ArrayList<PointerProperties> properties, int id) { int i = 0; for (PointerProperties prop : properties) { if (prop.id == id) { return i; } i++; } throw new RuntimeException("findIndex: touch id not found"); } private boolean performMultiPointerGesture(final PointerCoords[][] pcs) { // Each element in pcs represents a contact as a series of PointerCoords. // Events are injected with an interval of about 5ms. Some contacts might // end earlier than others (if a finger was released earlier), indicated by // a shorter list of events, or start later, indicated by null entries at // the beginning of the contact. boolean hasEvents = true, success = true; int step = 0; long downTime = 0; ArrayList<PointerProperties> properties = new ArrayList(); ArrayList<PointerCoords> coords = new ArrayList(); while (hasEvents) { hasEvents = false; // Lists of new/released pointer id's ArrayList<Integer> pointerDown = new ArrayList(); ArrayList<Integer> pointerUp = new ArrayList(); for (int id = 0; id < pcs.length; id++) { if (step < pcs[id].length) { hasEvents = true; if (pcs[id][step] != null) { if (step == 0 || pcs[id][step - 1] == null) { pointerDown.add(id); } } } else if (step == pcs[id].length) { pointerUp.add(id); } } for (int id : pointerUp) { int index = findIndex(properties, id); if (coords.size() == 1) { // If no more pointers will be touching the screen, we send the final ACTION_UP success &= injectPointers(downTime, MotionEvent.ACTION_UP, properties, coords); } else { success &= injectPointers(downTime, getPointerAction(MotionEvent.ACTION_POINTER_UP, index), properties, coords); } properties.remove(index); coords.remove(index); } if (coords.size() > 0) { for (int i = 0; i < coords.size(); i++) { coords.set(i, pcs[properties.get(i).id][step]); } success &= injectPointers(downTime, MotionEvent.ACTION_MOVE, properties, coords); } if (pointerDown.size() > 0) { if (coords.size() == 0) { // If no pointers are touching the screen, send first ACTION_DOWN int id = pointerDown.remove(0); downTime = SystemClock.uptimeMillis(); coords.add(pcs[id][step]); properties.add(fingerProperty(id)); success &= injectPointers(downTime, MotionEvent.ACTION_DOWN, properties, coords); } for (int id : pointerDown) { coords.add(pcs[id][step]); properties.add(fingerProperty(id)); success &= injectPointers(downTime, getPointerAction(MotionEvent.ACTION_POINTER_DOWN, coords.size() - 1), properties, coords); } } step++; SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS); } return success; } }